#include "base.h"

#include <library/unittest/registar.h>

#include <util/generic/ymath.h>
#include <util/string/cast.h>
#include <util/stream/output.h>
#include <util/system/compat.h>
#include <util/random/random.h>

#include <limits.h>

#if defined(_bionic_)
#include <time64.h>
#define timegm timegm64
#endif

struct TTestTime {
    const time_t T_ = 987654321;
    const char* Date_ = "Thu Apr 19 04:25:21 2001\n";
    const char* SprintDate_ = "20010419";
    const long SprintSecs_ = 15921;
    struct tm Tm_;
    TTestTime() {
        Tm_.tm_sec = 21;
        Tm_.tm_min = 25;
        Tm_.tm_hour = 4;
        Tm_.tm_mday = 19;
        Tm_.tm_mon = 3;
        Tm_.tm_year = 101;
        Tm_.tm_wday = 4;
        Tm_.tm_yday = 108;
    }
};

SIMPLE_UNIT_TEST_SUITE(TDateTimeTest){
    inline void OldDate8601(char* buf, time_t when){
        struct tm theTm;
struct tm* ret = nullptr;

ret = GmTimeR(&when, &theTm);

if (ret) {
    sprintf(buf, "%04d-%02d-%02dT%02d:%02d:%02dZ", theTm.tm_year + 1900, theTm.tm_mon + 1, theTm.tm_mday, theTm.tm_hour, theTm.tm_min, theTm.tm_sec);
} else {
    *buf = '\0';
}
}

SIMPLE_UNIT_TEST(Test8601) {
    char buf1[100];
    char buf2[100];

    for (size_t i = 0; i < 1000000; ++i) {
        const time_t t = RandomNumber<ui32>();

        OldDate8601(buf1, t);
        sprint_date8601(buf2, t);

        UNIT_ASSERT_VALUES_EQUAL(TStringBuf(buf1), TStringBuf(buf2));
    }
}

inline bool CompareTM(const struct tm& a, const struct tm& b) {
    return (
        a.tm_sec == b.tm_sec &&
        a.tm_min == b.tm_min &&
        a.tm_hour == b.tm_hour &&
        a.tm_mday == b.tm_mday &&
        a.tm_mon == b.tm_mon &&
        a.tm_year == b.tm_year &&
        a.tm_wday == b.tm_wday &&
        a.tm_yday == b.tm_yday);
}

static inline TString Str(const struct tm& a) {
    return TStringBuilder() << "("
                            << a.tm_sec << ", "
                            << a.tm_min << ", "
                            << a.tm_hour << ", "
                            << a.tm_mday << ", "
                            << a.tm_mon << ", "
                            << a.tm_year << ", "
                            << a.tm_wday << ", "
#if !defined(_musl_)
                            << a.tm_yday
#endif
                            << ")";
}

SIMPLE_UNIT_TEST(TestBasicFuncs) {
    ui64 mlsecB = millisec();
    ui64 mcrsecB = MicroSeconds();
    struct timeval tvB;
    gettimeofday(&tvB, nullptr);

    usleep(100000);

    ui64 mlsecA = millisec();
    ui64 mcrsecA = MicroSeconds();
    struct timeval tvA;
    gettimeofday(&tvA, nullptr);

    UNIT_ASSERT(mlsecB + 90 < mlsecA);
    UNIT_ASSERT((mcrsecB + 90000 < mcrsecA));
    //UNIT_ASSERT(ToMicroSeconds(&tvB) + 90000 < ToMicroSeconds(&tvA));
    //UNIT_ASSERT(TVdiff(tvB, tvA) == long(ToMicroSeconds(&tvA) - ToMicroSeconds(&tvB)));
}

SIMPLE_UNIT_TEST(TestCompatFuncs) {
    struct tm t;
    struct tm* tret = nullptr;
    TTestTime e;
    tret = gmtime_r(&e.T_, &t);
    UNIT_ASSERT(tret == &t);
    UNIT_ASSERT(CompareTM(e.Tm_, t));

    /*
         * strptime seems to be broken on Mac OS X:
         *
         *   struct tm t;
         *   char *ret = strptime("Jul", "%b ", &t);
         *   printf("-%s-\n", ret);
         *
         * yields "- -": ret contains a pointer to a substring of the format string,
         * that should never occur: function returns either NULL or pointer to buf substring.
         *
         * So this test fails on Mac OS X.
         */

    struct tm t2;
    Zero(t2);
    char* ret = strptime(e.Date_, "%a %b %d %H:%M:%S %Y\n ", &t2);
    UNIT_ASSERT(ret == e.Date_ + strlen(e.Date_));
    UNIT_ASSERT_VALUES_EQUAL(Str(e.Tm_), Str(t2));
    time_t t3 = timegm(&t);
    UNIT_ASSERT(t3 == e.T_);
}

SIMPLE_UNIT_TEST(TestSprintSscan) {
    char buf[256];
    long secs;
    TTestTime e;

    sprint_gm_date(buf, e.T_, &secs);
    UNIT_ASSERT(strcmp(buf, e.SprintDate_) == 0);
    UNIT_ASSERT(secs == e.SprintSecs_);

    struct tm t;
    Zero(t);
    bool ret = sscan_date(buf, t);
    UNIT_ASSERT(ret);
    UNIT_ASSERT(
        t.tm_year == e.Tm_.tm_year &&
        t.tm_mon == e.Tm_.tm_mon &&
        t.tm_mday == e.Tm_.tm_mday);

    buf[0] = 'a';
    ret = sscan_date(buf, t);
    UNIT_ASSERT(!ret);
}

SIMPLE_UNIT_TEST(TestNow) {
    i64 seconds = Seconds();
    i64 milliseconds = millisec();
    i64 microseconds = MicroSeconds();
    UNIT_ASSERT(Abs(seconds - milliseconds / 1000) <= 1);
    UNIT_ASSERT(Abs(milliseconds - microseconds / 1000) < 100);
    UNIT_ASSERT(seconds > 1243008607); // > time when test was written
}

SIMPLE_UNIT_TEST(TestStrftime) {
    struct tm tm;
    Zero(tm);
    tm.tm_year = 109;
    tm.tm_mon = 4;
    tm.tm_mday = 29;
    UNIT_ASSERT_STRINGS_EQUAL("2009-05-29", Strftime("%Y-%m-%d", &tm));
}

SIMPLE_UNIT_TEST(TestNanoSleep) {
    NanoSleep(0);
    NanoSleep(1);
    NanoSleep(1000);
    NanoSleep(1000000);
}

static bool TimeZoneEq(const char* zone0, const char* zone1) {
    if (strcmp(zone0, "GMT") == 0)
        zone0 = "UTC";
    if (strcmp(zone1, "GMT") == 0)
        zone1 = "UTC";
    return strcmp(zone0, zone1) == 0;
}

static bool CompareTMFull(const tm* t0, const tm* t1) {
    return t0 && t1 &&
           CompareTM(*t0, *t1) &&
           (t0->tm_isdst == t1->tm_isdst)
#ifndef _win_
           && (t0->tm_gmtoff == t1->tm_gmtoff) &&
           TimeZoneEq(t0->tm_zone, t1->tm_zone)
#endif // _win_
           && true;
}

SIMPLE_UNIT_TEST(TestGmTimeR) {
    time_t starttime = -12244089600L; // 1-Jan-1582
    time_t finishtime = (time_t)0xFFFFFFFF * 20;
    time_t step = time_t(0xEFFFFFFF);
    struct tm tms0, tms1;
    struct tm* ptm0 = nullptr;
    struct tm* ptm1 = nullptr;
    for (time_t t = starttime; t < finishtime; t += step) {
        ptm0 = GmTimeR(&t, &tms0);
        UNIT_ASSERT_EQUAL(ptm0, &tms0);

#ifdef _win_
        if (tms0.tm_year + 1900 > 3000) {
            // Windows: _MAX__TIME64_T == 23:59:59. 12/31/3000 UTC
            continue;
        }
#endif

        ptm1 = gmtime_r(&t, &tms1);
        if (!ptm1)
            continue;
        UNIT_ASSERT_EQUAL(ptm1, &tms1);
        UNIT_ASSERT(CompareTMFull(ptm0, ptm1));
    }
}
}
;

SIMPLE_UNIT_TEST_SUITE(DateTimeTest) {
    SIMPLE_UNIT_TEST(TestDurationFromFloat) {
        UNIT_ASSERT_EQUAL(TDuration::MilliSeconds(500), TDuration::Seconds(0.5));
        UNIT_ASSERT_EQUAL(TDuration::MilliSeconds(500), TDuration::Seconds(0.5f));
    }

    SIMPLE_UNIT_TEST(TestSecondsLargeValue) {
        unsigned int seconds = UINT_MAX;
        UNIT_ASSERT_VALUES_EQUAL(((ui64)seconds) * 1000000, TDuration::Seconds(seconds).MicroSeconds());
    }

    SIMPLE_UNIT_TEST(TestToString) {
#define CHECK_CONVERTIBLE(v)                                         \
    do {                                                             \
        UNIT_ASSERT_VALUES_EQUAL(v, ToString(TDuration::Parse(v)));  \
        UNIT_ASSERT_VALUES_EQUAL(v, TDuration::Parse(v).ToString()); \
    } while (0)
#if 0

        CHECK_CONVERTIBLE("10s");
        CHECK_CONVERTIBLE("1234s");
        CHECK_CONVERTIBLE("1234ms");
        CHECK_CONVERTIBLE("12ms");
        CHECK_CONVERTIBLE("12us");
        CHECK_CONVERTIBLE("1234us");
#endif

        CHECK_CONVERTIBLE("1.000000s");
        CHECK_CONVERTIBLE("11234.000000s");
        CHECK_CONVERTIBLE("0.011122s");
        CHECK_CONVERTIBLE("33.011122s");
    }

    SIMPLE_UNIT_TEST(TestFromString) {
        static const struct T {
            const char* const Str;
            const TDuration::TValue MicroSeconds;
            const bool Parseable;
        } tests[] = {
            {"0", 0, true},
            {"1", 1000000, true},
            {"2s", 2000000, true},
            {"3ms", 3000, true},
            {"x3ms", 0, false},
        };

        for (const T* t = tests; t != std::end(tests); ++t) {
            // FromString
            bool parsed = false;
            try {
                TDuration time = FromString<TDuration>(t->Str);
                parsed = true;
                UNIT_ASSERT_EQUAL(t->MicroSeconds, time.MicroSeconds());
            } catch (const yexception&) {
                UNIT_ASSERT_VALUES_EQUAL(parsed, t->Parseable);
            }
            // TryFromString
            TDuration tryTime;
            UNIT_ASSERT_VALUES_EQUAL(TryFromString<TDuration>(t->Str, tryTime), t->Parseable);
            if (t->Parseable) {
                UNIT_ASSERT_EQUAL(t->MicroSeconds, tryTime.MicroSeconds());
            }
        }
    }

    SIMPLE_UNIT_TEST(TestSleep) {
        // check does not throw
        Sleep(TDuration::Seconds(0));
        Sleep(TDuration::MicroSeconds(1));
        Sleep(TDuration::MilliSeconds(1));
    }

    SIMPLE_UNIT_TEST(TestInstantToString) {
        UNIT_ASSERT_VALUES_EQUAL(TString("2009-08-06T15:19:06.023455Z"), ToString(TInstant::Seconds(1249571946) + TDuration::MicroSeconds(23455)));
        UNIT_ASSERT_VALUES_EQUAL(TString("2009-08-06T15:19:06.023455Z"), (TInstant::Seconds(1249571946) + TDuration::MicroSeconds(23455)).ToString());
        UNIT_ASSERT_VALUES_EQUAL(TString("2009-08-06T15:19:06Z"), (TInstant::Seconds(1249571946) + TDuration::MicroSeconds(23455)).ToStringUpToSeconds());
    }

    SIMPLE_UNIT_TEST(TestInstantMath) {
        UNIT_ASSERT_VALUES_EQUAL(TInstant::Seconds(1719), TInstant::Seconds(1700) + TDuration::Seconds(19));
        // overflow
        UNIT_ASSERT_VALUES_EQUAL(TInstant::Max(), TInstant::Max() - TDuration::Seconds(10) + TDuration::Seconds(19));
        // underflow
        UNIT_ASSERT_VALUES_EQUAL(TInstant::Zero(), TInstant::Seconds(1000) - TDuration::Seconds(2000));
        UNIT_ASSERT_VALUES_EQUAL(TDuration::Zero(), TInstant::Seconds(1000) - TInstant::Seconds(2000));
    }

    SIMPLE_UNIT_TEST(TestDurationMath) {
        TDuration empty;
        UNIT_ASSERT(!empty);
        // ensure that this compiles too
        if (empty) {
            UNIT_ASSERT(false);
        }
        TDuration nonEmpty = TDuration::MicroSeconds(1);
        UNIT_ASSERT(nonEmpty);

        UNIT_ASSERT_VALUES_EQUAL(TDuration::Seconds(110), TDuration::Seconds(77) + TDuration::Seconds(33));
        // overflow
        UNIT_ASSERT_VALUES_EQUAL(TDuration::Max(), TDuration::Max() - TDuration::Seconds(1) + TDuration::Seconds(10));
        // underflow
        UNIT_ASSERT_VALUES_EQUAL(TDuration::Zero(), TDuration::Seconds(20) - TDuration::Seconds(200));
        // division
        UNIT_ASSERT_DOUBLES_EQUAL(TDuration::Minutes(1) / TDuration::Seconds(10), 6.0, 1e-9);
    }

    template <class T>
    void TestTimeUnits() {
        T withTime(1249571946000000L);
        T onlyMinutes(1249571940000000L);
        T onlyHours(1249570800000000L);
        T onlyDays(1249516800000000L);
        ui64 minutes = 20826199;
        ui64 hours = 347103;
        ui64 days = 14462;

        UNIT_ASSERT_VALUES_EQUAL(withTime.Minutes(), minutes);
        UNIT_ASSERT_VALUES_EQUAL(onlyMinutes, T::Minutes(minutes));
        UNIT_ASSERT_VALUES_EQUAL(onlyMinutes.Minutes(), minutes);

        UNIT_ASSERT_VALUES_EQUAL(withTime.Hours(), hours);
        UNIT_ASSERT_VALUES_EQUAL(onlyMinutes.Hours(), hours);
        UNIT_ASSERT_VALUES_EQUAL(onlyHours, T::Hours(hours));
        UNIT_ASSERT_VALUES_EQUAL(onlyHours.Hours(), hours);

        UNIT_ASSERT_VALUES_EQUAL(withTime.Days(), days);
        UNIT_ASSERT_VALUES_EQUAL(onlyHours.Days(), days);
        UNIT_ASSERT_VALUES_EQUAL(onlyDays, T::Days(days));
        UNIT_ASSERT_VALUES_EQUAL(onlyDays.Days(), days);
    }

    SIMPLE_UNIT_TEST(TestInstantUnits) {
        TestTimeUnits<TInstant>();
    }

    SIMPLE_UNIT_TEST(TestDurationUnits) {
        TestTimeUnits<TDuration>();
    }

    SIMPLE_UNIT_TEST(TestNoexceptConstruction) {
        UNIT_ASSERT_EXCEPTION(TDuration::MilliSeconds(FromString(STRINGBUF("not a number"))), yexception);
        UNIT_ASSERT_EXCEPTION(TDuration::Seconds(FromString(STRINGBUF("not a number"))), yexception);
    }
}
